Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
decentraland-bot
left a comment
There was a problem hiding this comment.
Review: approve ✅
This is a small change that follows existing patterns: navbar clicks made before analytics is ready now go through the same unload-safe beacon that useDownloadClick uses.
What I checked
- No double counting. Each click takes exactly one path. When analytics is ready it goes through
trackand returns; otherwise it goes through the beacon. A beacon click never enters the analytics-next buffer, so it cannot be sent again once Segment finishes loading. - The beacon follows the same rules that keep the SDK off.
- Bots:
@dcl/hooks@2.1.0does exportisBotClient, and itsAnalyticsProvideruses the same check to skip loading. - Exempt sessions:
postSegmentEventdeliberately skips the exempt-path check (bypassExemptPathGate), so the explicitisAnalyticsDisabledForSession()call is required, and it is there. - No writeKey:
postSegmentEventalready returns early. - Both checks run before
ensureSegmentAnonymousId(), so bots and exempt sessions never get anajs_anonymous_idcookie written.
- Bots:
- The navbar links are plain
hrefanchors (full page loads), so the beacon is the right transport for them. - Tests. The
afterEachwithjest.resetAllMocks()plus the newbeforeEachkeep each case isolated. The new cases cover a person, an exempt session, a bot, and analytics being ready. The ref pattern and the[track]dependencies matchuseDownloadClickanduseDeferredTrack. No unused imports.
Minor findings (P2, not blocking)
- P2:
sign_in,jump_inand middle-clicks into a new tab do not leave the page, but they also take the beacon path before analytics is ready. This includes the window where the SDK instance exists and would queue them with full context. Details inline. - P2: The download CTA beacon marks its events with
track_deferred: true(plustrack_called_atandtrack_delivered_at), but the navbar beacon does not. Details inline. - P2: No test covers a middle-click before analytics loads, or analytics becoming ready after the first render (a
rerenderbefore the click). The second is the case the ref exists for.
CI: lint, audit, title-matches-convention, lint-package-json and Vercel pass; test was still pending when I reviewed.
Conventions: the title feat: … and the branch feat/navbar-early-click-beacon follow ADR-6.
Consumer impact: none. This is an internal component change with no public surface.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| // links leave the page, so a click before that would be lost with it; the beacon survives the unload. It | ||
| // follows the same rules that keep the SDK off: sessions that started on an exempt page, and bots. | ||
| if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return | ||
| postSegmentEvent(SegmentEvent.CLICK, properties, ensureSegmentAnonymousId()) |
There was a problem hiding this comment.
[P2] Some navbar actions do not leave the page: sign_in (button), jump_in (button), and middle-clicks into a new tab (#905). Before analytics is ready they also take the beacon path, so they lose the full analytics-next context (traits, campaign, the SDK-generated messageId) that they would get if queued with useDeferredTrack. That includes the phase where the SDK instance exists but is still fetching settings. The cost is data quality, not lost data. If you want, keep the beacon for anchor clicks that navigate away and route the button and middle-click actions through useDeferredTrack. Fine to leave as is if the uniform payload matters more.
There was a problem hiding this comment.
Applied in 6bf7784. Only clicks that replace the page (a same-tab link, Sign In, which redirects) take the beacon now. Clicks that keep the page open (a target="_blank" link, ctrl/cmd/shift click, middle click, Jump In) go to useDeferredTrack, which queues them and sends them through analytics-next with full context once it is ready. The decision comes from isSameTabNavigation (helper with tests). The component spec covers both paths, including the queue draining when readiness flips.
| // links leave the page, so a click before that would be lost with it; the beacon survives the unload. It | ||
| // follows the same rules that keep the SDK off: sessions that started on an exempt page, and bots. | ||
| if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return | ||
| postSegmentEvent(SegmentEvent.CLICK, properties, ensureSegmentAnonymousId()) |
There was a problem hiding this comment.
[P2] useDownloadClick tags its beacon clicks with track_deferred: true (plus track_called_at and track_delivered_at), so the warehouse can tell beacon-sent Click events apart from SDK-sent ones. Here they can only be told apart through context.library.name = "dcl-sites-beacon". Consider adding at least track_deferred: true so dashboards can split navbar clicks the same way they split download clicks.
There was a problem hiding this comment.
Added in bf52e07: beacon-sent navbar clicks carry track_deferred: true, like useDownloadClick. Queued ones get it from useDeferredTrack. One caveat for dashboards: stg_landing_click / int_landing_tracks in DCL_NEXT only keep action, section and href from extra_data, so splitting there needs that allowlist widened. Until then, context.library.name = "dcl-sites-beacon" is the split available in the warehouse.
Coverage Report for CI Build 38311868329Coverage increased (+0.009%) to 94.19%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
decentraland-bot
left a comment
There was a problem hiding this comment.
Review
Verdict: Approve. I found no P0 or P1 issues.
The change does what the description says. Before analytics is ready, a click that leaves the page now goes through postSegmentEvent. A click that keeps the page open (target="_blank", ctrl/cmd/shift-click, middle-click, Jump In) is queued in useDeferredTrack and sent once Segment is ready. After that, clicks go through track.
What I checked
isBotClient(userAgent)is exported by@dcl/hooks2.1.0, and it is the same checkAnalyticsProvideruses to keep the SDK off for bots. Together withisAnalyticsDisabledForSession(), the beacon is skipped in the same cases where the SDK never loads. This is stricter thanuseDownloadClick, which skips the exempt-path check on purpose for conversion CTAs. That fits navbar clicks, which are not conversions.- Navbar links are plain
<a href>elements, so a same-tab click really does load a new page. The beacon is the right transport for these clicks, and each click is sent once only: the beacon branch returns before reaching the queue. ensureSegmentAnonymousId()creates and saves theajs_anonymous_idbefore Segment boots, so beacon clicks and later SDK events share the same anonymous id.trackNavbarnow reads readiness fromisInitializedRef, which matchesuseDownloadClick. RemovingisInitializedfrom the deps is correct.- The tests cover beacon, new tab then drain, exempt session, bot, and the ready path, plus the helper's modifier and
_blankcases.
Findings
- [P2] The beacon payload has
track_deferred: truebut nottrack_called_at/track_delivered_at.useDownloadClick's beacon branch and theuseDeferredTrackqueue both send all three (see the inline comment).
CI: lint, audit, and title checks pass. test was still running when I reviewed.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return | ||
| // Flagged like useDownloadClick's beacon clicks, so beacon-sent and SDK-sent clicks can be told apart. | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| postSegmentEvent(SegmentEvent.CLICK, { ...properties, track_deferred: true }, ensureSegmentAnonymousId()) |
There was a problem hiding this comment.
[P2] The comment says this is "flagged like useDownloadClick's beacon clicks", but that beacon (src/hooks/useDownloadClick.ts) also sends track_called_at and track_delivered_at, and so does every queued click from useDeferredTrack (including the new-tab path just above). As written, a navbar beacon Click is the only deferred click without those fields. Any query that uses them (for example, to measure the delay between the click and when it was sent) will get nulls for these rows. Suggested change to keep the payloads the same:
| postSegmentEvent(SegmentEvent.CLICK, { ...properties, track_deferred: true }, ensureSegmentAnonymousId()) | |
| const calledAt = Date.now() | |
| postSegmentEvent( | |
| SegmentEvent.CLICK, | |
| // eslint-disable-next-line @typescript-eslint/naming-convention | |
| { ...properties, track_called_at: calledAt, track_delivered_at: calledAt, track_deferred: true }, | |
| ensureSegmentAnonymousId() | |
| ) |
If you take this, the postSegmentEvent assertion in LandingNavbar.spec.tsx will need expect.any(Number) for the two timestamps.
Note: this suggestion replaces only line 287 and includes its own eslint-disable-next-line, so also delete the existing one on line 286 when you apply it.
There was a problem hiding this comment.
Applied in 78291c4: the beacon payload now carries track_called_at and track_delivered_at (both set to the click time, since the beacon is sent on the spot) alongside track_deferred: true. The spec asserts the timestamps with expect.any(Number).
decentraland-bot
left a comment
There was a problem hiding this comment.
Review: send pre-ready navbar clicks through the beacon
Verdict: Approve. I found no P0 or P1 issues. The approach is sound and every handler sends each click down exactly one path: track, the useDeferredTrack queue, or the beacon. Nothing is counted twice.
Verified
- Click classification matches what each handler actually does.
- Navbar links are plain
<a>elements, so a same-tab click does a full page load and the beacon is right. - Sign In ends in
redirectToAuth, which callswindow.location.replace. The page unloads, so the beacon is right. - Jump In calls
preventDefaultand opens the modal or launcher. The page stays, so the queue is right. DesktopTabWithDropdownuseswindow.open(url, '_self'), which always navigates the same tab, so the defaultleavesPage = trueis right.- Middle click goes through
onAuxClickwithfalse, so the queue is right.
- Navbar links are plain
- The beacon follows the same rules that keep the SDK off. In
@dcl/hooks2.1.0,AnalyticsProviderstays off only for an empty write key or a bot.- An empty write key makes
postSegmentEventreturn early. - Bots are checked with the same
isBotClient, which 2.1.0 exports for this purpose. - The
isAnalyticsDisabledForSession()check puts back the exempt-path rule thatpostSegmentEventskips on purpose. - Neither the SDK nor the beacon has a consent or Do Not Track check, so the beacon skips nothing the SDK applies.
- An empty write key makes
- Reading
isInitializedthrough a ref avoids beaconing a click with a stalefalseafter the render where Segment became ready. This is the same pattern asuseDownloadClick. - Security:
hrefandactioncome only from static navbar config, and the body is built withJSON.stringifyand sent astext/plainwithcredentials: 'omit'. No new PII or injection surface.
Findings (all non-blocking)
- [P2]
isSameTabNavigationignoresaltKey(inline). - [P2] The deferred-beacon payload is copied from
useDownloadClickand has already drifted from it (inline). - [P2] The positional
leavesPageboolean is hard to read at the call sites.trackNavbar('jump_in', undefined, false)reads better as{ leavesPage: false }. - [P2] Test gaps. The specs cover Learn (beacon), a new-tab link (queue), exempt sessions and bots. They don't cover:
- the Sign In beacon
- the
DesktopTabWithDropdownbeacon - the Jump In queue
- the payload in the "analytics is ready" case (for example, that
track_deferredis not set)
- [P2] Follow-up, out of scope here. The navbar is the only
postSegmentEventcaller that filters bots. The download beacons (useDownloadClick,downloadTracking,downloadPageExit,downloadFunnelExit, and others) still send for bots. Moving the bot check intopostSegmentEventwould apply one rule to every caller.
CI
lint, audit, lint-package-json, title convention and Vercel pass. test was still pending when this review was posted. I couldn't run the specs locally in this environment, so please confirm test is green before merging.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| */ | ||
| function isSameTabNavigation(event: { currentTarget: EventTarget; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }): boolean { | ||
| const target = event.currentTarget instanceof Element ? event.currentTarget.getAttribute('target') : null | ||
| return target !== '_blank' && !event.ctrlKey && !event.metaKey && !event.shiftKey |
There was a problem hiding this comment.
[P2] Alt-click (Option-click on macOS) on a link downloads it in Chrome and keeps the page open, but this helper counts it as leaving the page. Before analytics is ready, that click goes out by beacon instead of waiting in the queue for the SDK. It is still counted once, just without the SDK's full context. Consider adding altKey to the parameter type and the check (&& !event.altKey), plus an it.each case for it.
There was a problem hiding this comment.
Applied in 09c759c: isSameTabNavigation also treats altKey as keeping the page, and the it.each covers it.
| postSegmentEvent( | ||
| SegmentEvent.CLICK, | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| { ...properties, track_called_at: calledAt, track_delivered_at: calledAt, track_deferred: true }, |
There was a problem hiding this comment.
[P2] This copies the deferred-beacon payload from useDownloadClick.ts (track_called_at / track_delivered_at / track_deferred + ensureSegmentAnonymousId()), and the two have already drifted: here track_delivered_at reuses calledAt, while useDownloadClick takes a fresh Date.now(). A small shared helper (e.g. postDeferredClick(properties) next to postSegmentEvent) would keep the beacon callers identical, which the comment above says is the intent.
There was a problem hiding this comment.
Applied in b7dc1dc. New postDeferredClick(properties) in src/modules/deferredClickBeacon.ts (with a spec) builds the deferral fields once, and both useDownloadClick and the navbar call it. track_delivered_at now takes a fresh Date.now() for both, matching what useDownloadClick did before. useDownloadClick specs pass unchanged.
|
Summary-level P2s:
|
decentraland-bot
left a comment
There was a problem hiding this comment.
Summary
This is a well-scoped change. Every navbar call site is put on the right path:
- Clicks that replace the page go through the beacon. These are same-tab
<a>links (logo, Learn, credits, user menu, internal dropdown items), the section tab buttons (window.open(url, '_self')) and Sign In (redirectToAuth→location.replace). - Clicks that keep the page open wait in the
useDeferredTrackqueue. These aretarget="_blank"items, modifier clicks, middle clicks and Jump In (useHangOutActioncallspreventDefault).
Each branch returns early, so a click never goes through the beacon and also gets queued, and nothing is sent twice. The beacon gate (exempt-session check plus isBotClient) matches the rules that keep the SDK off. isBotClient is exported by the installed @dcl/hooks@2.1.0. Extracting postDeferredClick into its own module does not change what useDownloadClick sends.
Security: I found no issues. The beacon path checks the exempt-page gate and the bot check before sending. The href values come from fixed navbar config and carry no user input or wallet. userId is only present after the SDK has identified the user, the same as SDK-sent events.
Verification (local, head a379f0d): jest on LandingNavbar, deferredClickBeacon and useDownloadClick: 111/111 passing. tsc --noEmit: clean. eslint on the touched files: clean. CI checks were still pending when I posted this review.
Findings
All findings are P2. None blocks the merge.
- [P2]
LandingNavbar.helpers.ts:56: the parameter type is written inline in a.helpers.tsfile, which CLAUDE.md forbids ("Types / interfaces:<thing>.types.ts. Never inline in.client.ts,.helpers.ts…"). - [P2]
LandingNavbar.tsx:261: the ref comment claims more than the ref does. - [P2]
deferredClickBeacon.ts:8: the doc comment says callers apply the exempt and bot gates, butuseDownloadClickapplies neither. - [P2] Test gaps in
LandingNavbar.spec.tsx. These cases are not covered at the component level:- A middle click (
onAuxClick→ queue) before analytics is ready. - A ctrl/cmd click on a same-tab link through the component. Only the helper is tested for this.
- A beaconed click followed by analytics finishing, asserting that
trackis not called afterwards. This would lock in the no-double-send behavior.
- A middle click (
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| * Whether a primary click on a navbar link replaces the current page. A link that opens in a new tab | ||
| * (`target="_blank"`, or ctrl/cmd/shift held) or downloads it (alt/option held) leaves this page alive, so its click can wait for analytics. | ||
| */ | ||
| function isSameTabNavigation(event: { |
There was a problem hiding this comment.
[P2] CLAUDE.md does not allow inline types in .helpers.ts files. This hand-written shape could be replaced with Pick<React.MouseEvent, 'currentTarget' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'altKey'>, or a named type in LandingNavbar.types.ts. Either option also keeps the type in sync with the DOM event.
There was a problem hiding this comment.
Fixed in e709356: the event is now Pick<MouseEvent, 'currentTarget' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'altKey'> from React, so the instanceof Element guard is gone too.
| const { isInitialized, track } = useAnalytics() | ||
| const [mobileMenuOpen, setMobileMenuOpen] = useState(false) | ||
|
|
||
| // Read through a ref (as useDownloadClick does) so a click sees Segment's current readiness even if it |
There was a problem hiding this comment.
[P2] This comment claims more than the ref does. A ref assigned during render only ever holds the value from the last render, so a click cannot see a readiness change that has not been rendered yet. What the ref does is keep trackNavbar stable, so it no longer depends on isInitialized and doesn't invalidate the memoized handlers. Suggest rewording the comment to say that. useDownloadClick has the same wording, so it could be fixed there too.
There was a problem hiding this comment.
Fixed in d881b17: both comments (here and in useDownloadClick) now say the ref keeps the handler's identity when analytics becomes ready.
| /** | ||
| * Sends a `Click` made before analytics was ready through the unload-safe beacon, for clicks that leave the page. | ||
| * Adds the same deferral fields `useDeferredTrack` gives queued clicks, so every deferred click carries them | ||
| * whichever transport sent it. Callers apply their own gates (exempt session, bots) before calling. |
There was a problem hiding this comment.
[P2] useDownloadClick is the other caller, and it applies neither gate. Skipping the exempt-session check there is intentional, because /download is exempt but its CTAs must still be counted. Skipping the bot check, though, means a crawler that clicks a download CTA before Segment is ready still sends a beacon. This was already the case before this PR. Two options:
- Reword the comment so it does not promise gating that only the navbar does.
- Move the
isBotClient(navigator.userAgent)check intopostDeferredClick. Both callers would then share it, and the navbar would only keep its exempt-session check.
There was a problem hiding this comment.
Reworded in 916d4d4: the comment now says postDeferredClick applies no gate and names what each caller skips. I left the bot check where it is, because moving it into postDeferredClick would stop counting bot clicks on download CTAs and change the download funnel numbers. That goes in a separate PR, once the data team has seen it.
|
The test gaps from the last review are covered in ed2c8c3: a pre-ready middle click and ctrl/cmd click on a same-tab link are queued and drained once analytics is ready, and a beaconed click is not sent again through |
decentraland-bot
left a comment
There was a problem hiding this comment.
Review
Verdict: Approve. I found no P0 or P1 issues.
The routing is correct. Before analytics is ready, each click goes to exactly one transport (beacon or queue), never both. Once analytics is ready, every click goes through track as before. I checked each caller's leavesPage value:
- Leave the page: plain same-tab links, the desktop section tab (
window.open(url, '_self'), which stays in the same tab even with a modifier key held), and both Sign In buttons (useSignInRedirectalways callsredirectToAuth). - Keep the page:
_blanklinks, ctrl/cmd/shift/alt clicks, middle click, and Jump In (it only opens the download modal or the desktop app).
Moving the isInitialized read into a ref is safe: track keeps the same identity while analytics loads, so trackNavbar doesn't capture a stale value. The beacon gate (isAnalyticsDisabledForSession + isBotClient) uses the same conditions that keep the SDK off. It is needed because postSegmentEvent bypasses the exempt-path gate. The postDeferredClick extraction in useDownloadClick sends the same payload as before, and the download CTAs are still ungated on purpose.
Tests cover the beacon path, the queue for new-tab, modifier and middle clicks (including the flush with no double send), the exempt and bot sessions, Sign In, and all four modifier keys in isSameTabNavigation.
Minor (P2)
- P2
LandingNavbar.tsx:277: in exempt or bot sessions, clicks that keep the page are still pushed intouseDeferredTrack's queue. Analytics never loads there, so the queue is never flushed. Nothing is sent, but the closures stay in memory for the life of the component. See the inline comment. - P2 (by design, noting it for the data team): a same-tab click made while analytics.js is loaded but not ready goes out through the beacon, so it carries the beacon's context instead of the SDK's (no campaign/UTM enrichment). Analysis that splits on
track_deferred = trueshould keep this in mind.
CI: lint, lint-package-json, title and Vercel are passing. test and audit were still running when I posted this.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| } | ||
| // Analytics loads once the page is idle and is ready only after Segment fetched its settings. A click | ||
| // that stays on the page (new tab, Jump In) waits in the queue with the SDK's full context. | ||
| if (!leavesPage) { |
There was a problem hiding this comment.
[P2] In a session that started on an exempt page, or for a bot, analytics never initializes, so this queue is never flushed. Nothing is sent (correct), but each click that keeps the page leaves a closure in queueRef for the life of the navbar. Checking the gate before both branches makes the rule explicit and avoids that:
if (isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent)) return
if (!leavesPage) {
deferredTrack(SegmentEvent.CLICK, properties)
return
}
postDeferredClick(properties)This doesn't block the merge.
There was a problem hiding this comment.
Fixed in 898f3c7: the exempt-session and bot check now runs before both deferred paths, so those clicks are neither beaconed nor queued. A test for each case middle-clicks a link, forces readiness, and checks nothing is sent; both fail against the previous code.
|
On the second P2 (beacon context): agreed, a beaconed click has no |
decentraland-bot
left a comment
There was a problem hiding this comment.
Review: approve ✅
Verdict: approve. I found no P0 or P1 issues at 898f3c7.
What changed since the last round
898f3c7 moves the isAnalyticsDisabledForSession() || isBotClient(navigator.userAgent) check ahead of both deferred paths. In exempt sessions and for bots, clicks are now neither sent by beacon nor queued, so the queue no longer collects closures that never flush. The fix is correct. Both new tests middle-click, force readiness and check that nothing is sent. Middle-click is the right input to test, because before this commit it was the case that reached the queue.
What I checked
- Each caller is routed to the right path. I traced every
trackNavbar/trackNavbarLinkcaller:- Beacon (the click replaces the page): same-tab links (logo, Learn, dropdown items, credits chip, desktop and mobile user menu, mobile sub-items), section tabs (
window.open(url, '_self')) and both Sign In buttons (redirectToAuth). - Queue (the page stays open):
target="_blank"items, ctrl/cmd/shift/alt clicks, middle click (onAuxClick), and Jump In (it opens a modal or the launcher). - Keyboard: pressing Enter fires
clickwithbutton === 0and carries the modifier flags, so a keyboard click is classified the same way as a mouse click.
- Beacon (the click replaces the page): same-tab links (logo, Learn, dropdown items, credits chip, desktop and mobile user menu, mobile sub-items), section tabs (
- No double counting. Each branch returns early. A beacon click never enters the queue or the SDK buffer.
- The queue survives route changes.
LandingNavbaris rendered by the parent<Layout>route, so it stays mounted across nested routes and the component-scoped queue is kept. - Security (checked against the bug-bounty rules): no issues found. The beacon payload carries only static navbar config (
place,event,action,section,href) plus the standard context.userIdmatches what the SDK sends. Exempt sessions and bots are skipped beforeensureSegmentAnonymousId(), so they never get an anonymous-id cookie. - No public API change.
postDeferredClickis internal, anduseDownloadClicksends the same payload as before. - Local checks:
jeston LandingNavbar, deferredClickBeacon and useDownloadClick passed 120/120.tsc --noEmitandeslinton the touched files are clean. CI (test,lint,audit) was still running when I posted this.
Minor (P2, not blocking)
- P2:
LandingNavbar.spec.tsx:802. The Jump In drain test does not check how many times the click was sent (see the inline suggestion). - P2: If analytics never becomes ready for another reason (an ad blocker blocks the
settingsfetch, or the write key is empty), queued new-tab clicks are never sent. Same-tab clicks may still arrive by beacon. This is no worse than before the PR, when every such click was dropped. It does skew the split between new-tab and same-tab clicks in those sessions, which is worth knowing when reading the data. - P2: Not signed in, a visitor clicks Jump In before analytics is ready and then follows the download modal's link in the same tab, still before analytics is ready. The queued
jump_inclick is then lost when the page unloads. The window is narrow because analytics loads once the page is idle, so this is acceptable as is.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| }) | ||
|
|
||
| it('should send the queued click through analytics', () => { | ||
| expect(track).toHaveBeenCalledWith('Click', expect.objectContaining({ action: 'jump_in', track_deferred: true })) |
There was a problem hiding this comment.
[P2] This test checks that the queued jump_in click is sent, but it would also pass if the click were sent twice. The no-double-send guarantee is the main thing this flow has to keep, and the neighbouring analytics is ready cases already pin it with toHaveBeenCalledTimes(1). Adding the same assertion here, and expect(postSegmentEvent).not.toHaveBeenCalled(), would catch a regression where the drain also fires from another path.
| expect(track).toHaveBeenCalledWith('Click', expect.objectContaining({ action: 'jump_in', track_deferred: true })) | |
| expect(track).toHaveBeenCalledTimes(1) | |
| expect(track).toHaveBeenCalledWith('Click', expect.objectContaining({ action: 'jump_in', track_deferred: true })) |
There was a problem hiding this comment.
Fixed in 6cb5e96: the drain test now asserts one track call and no beacon.
|
On the other two P2s, no code change:
|
decentraland-bot
left a comment
There was a problem hiding this comment.
Review
This looks good. Each click goes through exactly one transport (beacon, queue, or track), so nothing is sent twice. The bot and exempt-session checks run before either deferred path, which fixes the case where a queued click would wait forever.
I checked how each call site is classified:
- Desktop section tab (
window.open(url, '_self')), logo, Learn, Credits, mobile links, user menu: these use the beacon on a plain click and the queue for_blankor modifier clicks. Middle clicks always use the queue. ✅ - Sign In (
redirectToAuthreplaces the location): beacon ✅ - Jump In (prevents the default action, opens the modal or deep link; the page stays): queue ✅
useDownloadClick→postDeferredClickis a straight extraction that sends the same payload ✅- Anonymous id: the beacon uses
ensureSegmentAnonymousId, which stores the id where analytics.js picks it up, so beacon events and later SDK events share one id. Bots and exempt sessions return before an id is created. ✅
The tests are thorough: beacon vs queue routing, exact payloads, ordering against window.open, both gates on both paths, and no double send once analytics is ready.
Findings (none block the merge):
- P2
LandingNavbar.tsx:281: the queue only lives as long as the component, so a queued click is lost if the page unloads before analytics is ready. Example: ctrl-click a link, then plain-click another. See the inline comment. - P2
LandingNavbar.helpers.ts:58: only_blankcounts as a new tab. - P2
LandingNavbar.spec.tsx:779:window.scrollYis overridden and never restored. - P2 nit: several
beforeEach(async …)blocks in the spec areasyncbut don't await anything (one awaits only some of its steps). This is harmless.
CI: audit, lint, lint-package-json, title and Vercel pass. test was still pending at review time.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| // Analytics loads once the page is idle and is ready only after Segment fetched its settings. A click | ||
| // that stays on the page (new tab, Jump In) waits in the queue with the SDK's full context. | ||
| if (!leavesPage) { | ||
| deferredTrack(SegmentEvent.CLICK, properties) |
There was a problem hiding this comment.
[P2] The useDeferredTrack queue only lives as long as the component, so a queued click is dropped if the page unloads before analytics is ready. For example: ctrl-click Creator docs, then plain-click Learn within the same second; or Jump In, then follow the modal's link to /download. The plain click goes out by beacon, but the queued one is lost. The window is narrow, so this doesn't block the merge. If you want to close it, flush any pending queue entries through postDeferredClick on pagehide.
There was a problem hiding this comment.
Leaving this one as is. The queue belongs to useDeferredTrack, which 13 other components share (places cards, section-viewed, link context, Discover). A pagehide flush in the hook would send all of their queued events by beacon, skipping the bot and exempt-session rules the SDK applies to them today, and would change data outside this PR. A navbar-only flush would mean a second queue. The lost click needs two clicks in the window before settings loads, and before this PR every one of those clicks was dropped.
| * (`target="_blank"`, or ctrl/cmd/shift held) or downloads it (alt/option held) leaves this page alive, so its click can wait for analytics. | ||
| */ | ||
| function isSameTabNavigation(event: Pick<MouseEvent, 'currentTarget' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'altKey'>): boolean { | ||
| const target = event.currentTarget.getAttribute('target') |
There was a problem hiding this comment.
[P2] Only _blank counts as opening elsewhere. That matches today's config (links render either _blank or no target). A named target such as target="docs" added later would also keep the page alive but would be sent by beacon. Checking !target || target === '_self' would make the intent explicit.
There was a problem hiding this comment.
Fixed in 2014d91: only no target, _self, _parent and _top (case-insensitive) count as replacing the page. _blank and any named target keep it, so their clicks are queued. Tests cover both groups.
|
|
||
| beforeEach(async () => { | ||
| onClickJumpIn = jest.fn() | ||
| Object.defineProperty(window, 'scrollY', { configurable: true, value: 100 }) |
There was a problem hiding this comment.
[P2] window.scrollY is redefined here and never restored, so the value carries into later tests. The existing override near line 296 has the same problem. Consider restoring the original descriptor in an afterEach.
There was a problem hiding this comment.
Fixed in d915708: a scrollPageTo helper sets window.scrollY, and the top-level afterEach restores the original descriptor. Both overrides, including the older one, go through it.
|
Last review round:
Also in this push: the click link and options types moved to |
decentraland-bot
left a comment
There was a problem hiding this comment.
Summary
Approve. This PR routes navbar clicks made before analytics is ready by whether the page survives the click. A click that replaces the page (same-tab link, Sign In, section tabs) goes out by beacon. A click that keeps the page (_blank or a named target, ctrl/cmd/shift/alt click, middle click, Jump In) waits in the useDeferredTrack queue. Exempt sessions and bots send nothing. Once analytics is ready, every click goes through track as before.
What I checked
- Every tracked navbar element (logo, Learn, Credits chip, dropdown items, user menu items, section tabs, Sign In, Jump In) takes the right path:
- Same-tab
<a>links and section tabs (window.open(url, '_self')) take the beacon. - Sign In takes the beacon, since
redirectToAuthcallslocation.replace. - Jump In is queued, since it calls
preventDefaultand the page stays. - The notification bell, notification items, the avatar toggle, the hamburger menu and Sign Out are not tracked, so none of them can beacon by mistake.
- Same-tab
- No double send: a beaconed click is never also queued, the queue is cleared before it is replayed, and the tests assert one
trackcall and no beacon. - Same rules as the SDK: the exempt-session and bot checks match the two conditions under which
AnalyticsProviderstays off.postSegmentEventstill returns early when the write key is empty. - No consent bypass: the repo has no consent, cookie-banner or Do-Not-Track code that the beacon could skip.
hrefexposes nothing private: it always comes from fixed values innavbarConfig.tsor constants inLandingNavbar.tsx.useDownloadClickrefactor: it keeps the same behaviour. The payload, the timestamps and the anonymous-id handling are unchanged, now inpostDeferredClick.- Locally:
tsc -b, eslint on the changed files and jest (7 suites, 131 tests) all pass. CI was still pending when I reviewed.
Findings (P2 only, none blocking)
- [P2]
src/modules/deferredClickBeacon.ts:24:ensureSegmentAnonymousId()runs beforepostSegmentEventchecks the write key (see the inline comment). - [P2]
src/modules/segmentBeacon.ts:147-151(not in this diff): theNOTEthere says every caller of this transport fires an explicit conversion event (download CTA click, funnel exit, download_*). That is no longer true now that navbar clicks use it too. Consider updating the comment so the reason it skips the exempt-path gate still holds, and noting that the navbar applies the gate before it calls.
I did not repeat findings from earlier rounds that were resolved or deliberately left as they are, such as the per-component queue being lost when the page unloads.
Reviewed by Jarvis 🤖 · Requested by braianj via GitHub
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| track_deferred: true | ||
| }, | ||
| ensureSegmentAnonymousId() |
There was a problem hiding this comment.
[P2] ensureSegmentAnonymousId() is evaluated as an argument, so it runs before postSegmentEvent checks the write key.
When SEGMENT_KEY is not set (local builds, or previews without the key), every pre-ready click still writes the one-year ajs_anonymous_id cookie and localStorage entry, then sends nothing. The SDK path writes nothing in that case.
In production the SDK writes the same id a few seconds later, so the only effect there is that the cookie appears earlier. The download CTA already had this order, but this PR applies it to many more clicks.
Possible fix: have postSegmentEvent resolve the anonymous id itself after its key check, or check the key here before calling ensureSegmentAnonymousId().
Navbar clicks made before analytics is ready are now sent instead of dropped. Analytics loads once the page is idle, and since
@dcl/hooks2.1.0 it reports ready only after Segment has fetched its settings, so until then the click used to be lost.Before analytics is ready, sessions that started on an exempt page (
isAnalyticsDisabledForSession) and bots (isBotClient) send nothing, the same rules that keep the SDK off. Otherwise, a click that replaces the page (a same-tab link, Sign In) goes through the unload-safe beacon the download CTAs use (postSegmentEvent), with the sametrack_deferred/track_called_at/track_delivered_atfields as every other deferred click. A click that keeps the page open (a new-tab link, ctrl/cmd/shift/alt click, middle click, Jump In) waits inuseDeferredTrack's queue and is sent by analytics.js with its full context once it is ready. Once analytics is ready, every click goes throughtrackas today.Planned for the next release.
How to test (preview, desktop Chrome, a new incognito window, DevTools Network filtered to
api.e.decentraland.org, throttling set to Slow 4G):/eventsand click Learn within the first second, before thesettingsrequest finishes. Expected: a POST to/v1/track(the beacon endpoint) goes out before thesettingsrequest finishes, and the blog loads.Clicksent by analytics.js (/v1/t), not a beacon./privacyin a new incognito window, wait a few seconds, then click Learn. Expected: noClickrequest (no/v1/trackor/v1/t).needs manual verification: open/eventsunder Slow 4G, hover Create and immediately click Creator documentation (new tab). Expected: no/v1/trackbeacon. Oncesettingsloads, aClickwithtrack_deferred: truegoes out through/v1/t. A component test covers this.